Vue3.2x源码解析(一):vue初始化过程

您所在的位置:网站首页 vue 审批流程插件 Vue3.2x源码解析(一):vue初始化过程

Vue3.2x源码解析(一):vue初始化过程

2023-03-25 03:41| 来源: 网络整理| 查看: 265

Vue3.2x源码解析(一):vue初始化过程1,目录结构

先回顾下 Vue2 源码目录:

├── src ├── compiler # 编译相关的模块 ├── core # 核心语法代码 ├── platforms # 平台相关 ├── server # 服务端渲染相关 ├── sfc # vue单文件组件 ├── shared # 公用方法

再看看 Vue3 源码的目录结构:

├──packages ├── compiler-core # 平台无关的编译器核心代码,baseParse生成AST ├── compiler-dom # 基于compiler-core,针对浏览器的附加插件的编译器 ├── compiler-sfc # 编译vue单文件组件 ├── compiler-ssr # 服务端渲染相关的 ├── reactivity # vue3响应式模块,例如:ref/reactive/effect ├── runtime-core # 平台无关的运行时核心代码,组件创建/渲染/更新 ├── runtime-dom # 基于runtime-core,针对浏览器的运行时,处理各种原始dom_api ├── vue # 面向公众的完整构建,其中包含编译器和运行时 ├── shared # 多个包共享的内部工具,公用方法 ├── ...

Vue3采用monorepo是管理项目代码的方式。不同于 Vue2 代码管理,它是在一个 repo中管理多个package项目,每个 package 都有自己的类型声明、单元测试。 package 又可以独立发布,这种部署方式更便于项目的维护和发版。

注意:vue3源码由TS代码编写,阅读源码前请先熟悉TS基本语法。vue3的强大渲染器可以实现多端跨平台的应用,本文依然以web端为视角进行源码解析。

2,源码入口

vue3每一个项目的开始,都是从vue中引入一个createApp方法,使用这个方法创建一个vue实例,这个实例就是我们的应用实例,一个项目可以按需求创建多个vue实例。

import { createApp } from 'vue' import App from './App.vue' // 应用初始化 const app = createApp(App) app.mount('#app') 这里的App是根组件,作为渲染组件的起点。mount('#app') 表示应用挂载的DOM节点容器。

createApp

接下来就从import { createApp } from 'vue'这句代码开始源码的学习。

首先查看createApp的源码:

// packages/runtime-dom/src/index.ts ​ # 创建vue应用实例 export const createApp = ((...args) => { // 创建vue实例 const app = ensureRenderer().createApp(...args) // 取出mount方法 const { mount } = app # 重写mount方法 app.mount = (containerOrSelector: Element | ShadowRoot | string): any => { // 确定挂载节点容器 const container = normalizeContainer(containerOrSelector) // 没有容器直接return 无法挂载应用 if (!container) return // 取出根组件 const component = app._component if (!isFunction(component) && !component.render && !component.template) { // 如果根组件没有render/template,则把容器的内容赋值给根组件 component.template = container.innerHTML } ​ // clear content before mounting // 加载之前清空dom容器内容 container.innerHTML = '' # 调用mount开始应用真正的加载 const proxy = mount(container, false, container instanceof SVGElement) if (container instanceof Element) { // 为容器节点设置属性 container.removeAttribute('v-cloak') container.setAttribute('data-v-app', '') } return proxy } // 返回应用实例 return app })

createApp源码内容看似不多,实则里面执行了非常多的逻辑,后面我们会一直跳转回来查看方法里面的内容。

首先分析第一行代码:

const app = ensureRenderer().createApp(...args)

一进来就执行了一个ensureRenderer方法。

ensureRenderer

我们理解一下这个方法的作用:

// packages/runtime-dom/src/index.ts # 确定渲染器 function ensureRenderer() { // 返回一个渲染器对象 return ( renderer || (renderer = createRenderer(rendererOptions)) ) }

可以看出这个方法只有一个作用,返回一个确定的渲染器renderer对象。

在讨论createRenderer之前,有一个重点要关注一下rendererOptions:

const rendererOptions = /*#__PURE__*/ extend({ patchProp }, nodeOps)

rendererOptions由两部分组成:

patchProp:处理元素的props、Attribute、class、style、event事件等。nodeOps:处理dom节点,这个对象里面包含了各种原生dom操作方法。

rendererOptions对象最终会传递到渲染器里面,里面的各种方法最终会在页面渲染的过程中被调用。

(一)renderer渲染器的类型

在知道renderer渲染器如何创建之前,我们先看看renderer对象的类型:

# Renderer web端渲染器 HydrationRenderer服务器端渲染器 let renderer: Renderer | HydrationRenderer 注意:Hydration开头的相关函数,为SSR服务器端渲染使用,后续会在Vue3源码中多次遇见。

可以看出renderer渲染器可以是web端渲染器,也可以是SSR渲染器,我们在浏览器中使用Vue框架就会确定为web端的渲染器。

(二)rederer渲染器的创建

接下来我们就分析renderer渲染器的创建过程:

// packages/runtime-dom/src/index.ts renderer = createRenderer(rendererOptions)

查看createRenderer源码:

// packages/runtime-core/src/renderer.ts # 创建渲染器 function createRenderer (options: RendererOptions) { return baseCreateRenderer(options) } # 注意;泛型函数 fn() {} // vue3源码量非常大,并且其中有非常多的泛型函数,在刚开始阅读源码的时候,可以减少关注具体的类型,重点放在逻辑过程。可以在后面解惑的时候再回头看具体的类型,这可以减轻刚开始阅读源码的压力。

baseCreateRenderer

继续查看baseCreateRenderer源码:

// packages/runtime-core/src/renderer.ts # Vue3渲染器核心: 非常重要 function baseCreateRenderer(options, createHydrationFns?) { // 确定全局对象 const target = getGlobalThis() target.__VUE__ = true # rendererOptions对象传递过来的原生dom操作方法 const { insert: hostInsert, remove: hostRemove, patchProp: hostPatchProp, createElement: hostCreateElement, createText: hostCreateText, createComment: hostCreateComment, setText: hostSetText, setElementText: hostSetElementText, parentNode: hostParentNode, nextSibling: hostNextSibling, setScopeId: hostSetScopeId = NOOP, insertStaticContent: hostInsertStaticContent } = options # 渲染器内部有几十个工具函数,这里列出几个常用的 // patch,根据vnode的类型,执行不同的逻辑, const patch = () => {} // 加载组件 const mountComponent = () => {} // 更新组件 const mountComponent = () => {} // 设置组件渲染renderEffect, 类似于vue2 watcher的renderWatcher const setupRenderEffect = () => {} # 渲染应用方法 重点 const render = () => {} return { render, hydrate, # 重点:初始化createApp方法 createApp: createAppAPI(render, hydrate) } } 注意:baseCreateRenderer是基础渲染器,功能非常强大,是Vue3应用渲染的核心,可以创建出不同环境需要的渲染器,自定义渲染器,源码量非常大,这里我们主要介绍渲染器内部的一些重点方法的用途,后面会根据场景再展开每个方法的具体逻辑。

baseCreateRenderer源码量非常多,这里直接看return返回值,返回了一个对象,这就是我们所需要的渲染器对象。

// 渲染器对象有三个方法,主要关注render和createApp const renderer = { render, hydrate, // 忽略 createApp: createAppAPI(render, hydrate) }

再跳回到ensureRenderer方法,可以看到这个方法的返回值就是渲染器对象renderer。

重点关注渲染器对象的render方法和createApp方法。

再跳回到刚开始的createApp源码的第一行代码:

createApp() { const app = ensureRenderer().createApp(...args) }

ensureRenderer生成一个渲染器后就立即调用内部的createApp方法,创建了一个vue应用实例,我们要理解Vue实例的具体创建过程,就得继续查看createApp方法的源码createAppAPI。

createAppAPI// packages/runtime-core/src/apiCreateApp.ts # 创建vue应用方法 function createAppAPI( render: RootRenderFunction, hydrate?: RootHydrateFunction ): CreateAppFunction { # 返回值就是createApp方法源码 return function createApp(rootComponent, rootProps = null) { // 参数为根组件:即src/App.vue组件 if (!isFunction(rootComponent)) { rootComponent = { ...rootComponent } } ​ if (rootProps != null && !isObject(rootProps)) { __DEV__ && warn(`root props passed to app.mount() must be an object.`) rootProps = null } ​ # 创建vue应用实例上下文 就是确定应用的一些默认配置 const context = createAppContext() // 初始化插件集合,用于存储插件列表 const installedPlugins = new Set() // 初始化应用挂载状态,默认为false let isMounted = false ​ # 创建vue实例对象 // 同时将应用实例存储到context上下文的app属性 const app: App = (context.app = { # 初始化一些应用属性 _uid: uid++, // 项目中可能存在多个vue实例,需使用id标识 _component: // 根组件 _props: rootProps, // 传递给根组件的props _container: null, // dom容器 _context: context, // app上下文 _instance: null, // 应用实例 version, // 版本 # 定义了一个访问器属性app.config,只能读取,不能直接替换 get config() { // 读取的是上下文的config return context.config }, set config(v) {}, # 下面是vue应用内部定义的几个方法 // 安装插件 use() {} // 混入 mixin() {} // 注册全局组件 component() {} // 注册自定义指令 directive() {} // 挂载应用 mount() {} // 卸载应用 unmount() {} // 全局数据 provide() {} }) if (__COMPAT__) { // 若开启兼容,安装vue2相关的API installAppCompatProperties(app, context, render) } # 返回vue实例对象 return app } }

以上就是createApp的源码,简单来说就是内部创建了一个vue实例对象,这个对象内部初始化了一些全局的属性和方法,和vue2源码中的initGlobalAPI逻辑比较类似,比如use/mixin/component/directive,这些都是我们比较熟悉的全局方法了。然后根据配置处理vue2兼容的相关API,最后返回了实例对象。

以上的内容就是刚开始createApp源码中第一行代码的逻辑:

createApp() { const app = ensureRenderer().createApp(...args) ... } // 所以我们在项目只使用了一行代码就创建一个vue应用,实际上框架内部已经执行了非常多的逻辑 const app = createApp(App)

我们再跳回到刚开始的createApp源码:

createApp() { const app = ensureRenderer().createApp(...args) # 1,取出原来的mount方法 const { mount } = app // 2,重写app的mount方法 app.mount = (containerOrSelector: Element | ShadowRoot | string): any => { ... # 调用mount开始应用真正的加载 const proxy = mount(container, false, container instanceof SVGElement) return proxy } return app }

在创建vue应用实例后,取出mount方法,然后立即重写了app实例的mount加载方法,最后返回了vue应用实例。

这里有一个重点是两个Mount方法的区别:

第一个mount方法为:vue应用的渲染,侧重为调用render方法进行应用渲染。第二个mount方法为:vue应用的挂载,侧重为将应用挂载到container节点容器。

这里的加载执行顺序是:先获取目标DOM节点容器,进行应用挂载,然后调用render方法进行应用渲染。即先执行重写的mount方法,然后在内部执行原来的mount加载方法。

const app = createApp(App) # 当然,一般在执行mount加载之前,都会注册一些全局组件和插件资源 app.component() app.directive() app.use(Element) app.use(pinia) # 加载应用 app.mount('#app')

分析到这里,以上的内容就是createApp源码的基本内容。主要作用就是确定渲染器,创建一个Vue应用实例,最后进行加载,下面我们将继续分析vue加载的详细过程。

3,应用加载# 1,执行重写的mount,应用挂载 app.mount = (containerOrSelector: Element | ShadowRoot | string): any => { ... # 2,执行原来的mount,应用渲染 const proxy = mount(container, false, container instanceof SVGElement) return proxy }

根据前面的分析,两个加载的执行顺序及内容我们都了解了,下面我们分析应用加载渲染的具体过程:

// packages/runtime-core/src/apiCreateApp.ts # 应用加载 mount(rootContainer: HostElement,isHydrate?: boolean,isSVG?: boolean): any { # 首次isMounted为false,执行加载渲染 if (!isMounted) { # 创建根组件vnode对象,默认编译生成的组件对象只有setup和render方法 const vnode = createVNode( rootComponent as ConcreteComponent, rootProps ) // store app context on the root VNode. // this will be set on the root instance on initial mount. // 将app的上下文存储在根虚拟节点 vnode.appContext = context ​ // 开发环境下的热更新 if (__DEV__) { context.reload = () => { render(cloneVNode(vnode), rootContainer, isSVG) } } // isHydrate水合函数与ssr有关 if (isHydrate && hydrate) { hydrate(vnode as VNode, rootContainer as any) } else { # 开始应用渲染 render(vnode, rootContainer, isSVG) } # 应用渲染完成,设置应用为已挂载状态 isMounted = true // 设置应用容器节点#app app._container = rootContainer // for devtools and telemetry 开发者工具检测 (rootContainer as any).__vue_app__ = app ​ if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { // 开发模式下:初始化开发者工具 app._instance = vnode.component devtoolsInitApp(app, version) } # 加载完成,返回应用的代理对象 return getExposeProxy(vnode.component!) || vnode.component!.proxy } }

上面的应用加载过程主要有三个重点:

创建根组件vnode对象。执行render方法开始渲染应用【整个应用的渲染过程都在里面】。渲染完成,设置应用为已挂载状态。

其中最重要的就是渲染逻辑,下面我们继续分析render方法:

// packages/runtime-core/src/Renderer.ts # 渲染方法 const render: RootRenderFunction = (vnode, container, isSVG) => { if (vnode == null) { if (container._vnode) { unmount(container._vnode, null, null, true) } } else { # 第一次vnode是根组件的内容,开始应用的渲染 // 首次container还没有_vnode属性为underfined ,即旧的_vnode为null patch(container._vnode || null, vnode, container, null, null, null, isSVG) } # 执行冲刷任务 flushPreFlushCbs() flushPostFlushCbs() // 定义_vnode属性:存储当前vnode container._vnode = vnode }

到这里,我们就可以对Vue应用的初始化过程做一个总结,简单来说就是:

创建Vue应用实例。确定应用挂载容器节点。创建根组件vnode对象。执行render加载渲染应用。

在生产环境下:Vue应用的初始化只会执行一次,除非主动刷新页面。

在开发环境下:由于源码变化热更新的支持,Vue应用可以多次执行初始化渲染。

// 开发环境下的热更新 if (__DEV__) { context.reload = () => { render(cloneVNode(vnode), rootContainer, isSVG) } }

下节我们继续分析patch里面的内容及组件的初始化过程。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3